const semver = require("semver");
const chalk = require("chalk");
const logSymbols = require("log-symbols");
const child_process = require("child_process");
const ora = require("ora");
const download = require("download-git-repo");
const { escape } = require("querystring");
const path = require("path");
const fs = require("fs");
const LRU = require("lru-cache");

const boolCNorEN = (context) => {
  return escape(context).indexOf("%u") > -1
    ? "cn"
    : /^[a-zA-Z]+$/.test(context)
    ? "en"
    : "unknown";
};
const utilError = (err, name, bool) => {
  (process.env.DEBUG === "true" || bool) && (console.log(""), log(name, err));
};
const win32 = process.platform === "win32";
const unixifyPath = (filepath) => {
  if (win32) {
    return filepath.replace(/\\/g, "/");
  } else {
    return filepath;
  }
};
const expTest = /\.(json|js|vue|less|scss|ts|html)$/;
const recurse = (rootdir, callback, subdir) => {
  var abspath = subdir ? path.join(rootdir, subdir) : rootdir;
  fs.readdirSync(abspath).forEach(function (filename) {
    var filepath = path.join(abspath, filename);
    if (fs.statSync(filepath).isDirectory()) {
      recurse(
        rootdir,
        callback,
        unixifyPath(path.join(subdir || "", filename || ""))
      );
    } else {
      if (expTest.test(filepath)) {
        callback(unixifyPath(filepath), rootdir, subdir, filename);
      }
    }
  });
};
const stringCase = (str) => {
  return boolCNorEN(str) === "en"
    ? str.toLowerCase().replace(/( |^)[a-z]/g, (L) => L.toUpperCase())
    : str;
};
const setTypeColor = (type) => {
  switch (type) {
    case "error":
      return "red";
      break;
    case "warn":
    case "debug":
      return "yellow";
      break;
    case "info":
      return "blue";
      break;
    default:
      return "green";
      break;
  }
};
const log = (type, err) => {
  type !== "tip"
    ? console.log(
        logSymbols[/warn|debug/.test(type) ? "warning" : type],
        chalk[setTypeColor(type)](
          type && type !== "info"
            ? ["[" + stringCase(type) + "]", err.message || err].join(" ")
            : [err.message || err].join(" ")
        )
      )
    : console.log([err.message || err].join(" "));
};

const browsers = {};

const isLinux = "linux" === process.platform;
const isMacintosh = "darwin" === process.platform;
const isWindows = process.platform.startsWith("win");

function tryRun(cmd) {
  try {
    return execSync(cmd, {
      stdio: [0, "pipe", "ignore"],
      timeout: 10000,
    })
      .toString()
      .trim();
  } catch (e) {
    return "";
  }
}

function getLinuxAppVersion(binary) {
  return tryRun(`${binary} --version`).replace(/^.* ([^ ]*)/g, "$1");
}

function getMacAppVersion(bundleIdentifier) {
  const bundlePath = tryRun(
    `mdfind "kMDItemCFBundleIdentifier=='${bundleIdentifier}'"`
  );

  if (bundlePath) {
    return tryRun(
      `/usr/libexec/PlistBuddy -c Print:CFBundleShortVersionString ${bundlePath.replace(
        /(\s)/g,
        "\\ "
      )}/Contents/Info.plist`
    );
  }
}

let _hasYarn;
const _yarnProjects = new LRU({
  max: 10,
  ttl: 1000,
});

const hasYarn = () => {
  if (process.env.VUE_CLI_TEST) {
    return true;
  }
  if (_hasYarn != null) {
    return _hasYarn;
  }
  try {
    execSync("yarn --version", { stdio: "ignore" });
    return (_hasYarn = true);
  } catch (e) {
    return (_hasYarn = false);
  }
};

const hasProjectYarn = (cwd) => {
  if (_yarnProjects.has(cwd)) {
    return checkYarn(_yarnProjects.get(cwd));
  }

  const lockFile = path.join(cwd, "yarn.lock");
  const result = fs.existsSync(lockFile);
  _yarnProjects.set(cwd, result);
  return checkYarn(result);
};

function checkYarn(result) {
  if (result && !hasYarn())
    throw new Error(
      `The project seems to require yarn but it's not installed.`
    );
  return result;
}

let _hasPnpm;
let _pnpmVersion;
const _pnpmProjects = new LRU({
  max: 10,
  ttl: 1000,
});

function getPnpmVersion() {
  if (_pnpmVersion != null) {
    return _pnpmVersion;
  }
  try {
    _pnpmVersion = execSync("pnpm --version", {
      stdio: ["pipe", "pipe", "ignore"],
    }).toString();
    // there's a critical bug in pnpm 2
    // https://github.com/pnpm/pnpm/issues/1678#issuecomment-469981972
    // so we only support pnpm >= 3.0.0
    _hasPnpm = true;
  } catch (e) {}
  return _pnpmVersion || "0.0.0";
}

const hasPnpmVersionOrLater = (version) => {
  if (process.env.VUE_CLI_TEST) {
    return true;
  }
  return semver.gte(getPnpmVersion(), version);
};

const hasPnpm3OrLater = () => {
  return hasPnpmVersionOrLater("3.0.0");
};

const hasProjectPnpm = (cwd) => {
  if (_pnpmProjects.has(cwd)) {
    return checkPnpm(_pnpmProjects.get(cwd));
  }

  const lockFile = path.join(cwd, "pnpm-lock.yaml");
  const result = fs.existsSync(lockFile);
  _pnpmProjects.set(cwd, result);
  return checkPnpm(result);
};

function checkPnpm(result) {
  if (result && !hasPnpm3OrLater()) {
    throw new Error(
      `The project seems to require pnpm${
        _hasPnpm ? " >= 3" : ""
      } but it's not installed.`
    );
  }
  return result;
}

const _npmProjects = new LRU({
  max: 10,
  ttl: 1000,
});
const hasProjectNpm = (cwd) => {
  if (_npmProjects.has(cwd)) {
    return _npmProjects.get(cwd);
  }

  const lockFile = path.join(cwd, "package-lock.json");
  const result = fs.existsSync(lockFile);
  _npmProjects.set(cwd, result);
  return result;
};

const SUPPORTED_PACKAGE_MANAGERS = ["yarn", "pnpm", "npm"];
const PACKAGE_MANAGER_PNPM4_CONFIG = {
  install: ["install", "--reporter", "silent", "--shamefully-hoist"],
  add: ["install", "--reporter", "silent", "--shamefully-hoist"],
  upgrade: ["update", "--reporter", "silent"],
  remove: ["uninstall", "--reporter", "silent"],
};
const PACKAGE_MANAGER_PNPM3_CONFIG = {
  install: ["install", "--loglevel", "error", "--shamefully-flatten"],
  add: ["install", "--loglevel", "error", "--shamefully-flatten"],
  upgrade: ["update", "--loglevel", "error"],
  remove: ["uninstall", "--loglevel", "error"],
};
const PACKAGE_MANAGER_CONFIG = {
  npm: {
    install: ["install", "--loglevel", "error"],
    add: ["install", "--loglevel", "error"],
    upgrade: ["update", "--loglevel", "error"],
    remove: ["uninstall", "--loglevel", "error"],
  },
  pnpm: hasPnpmVersionOrLater("4.0.0")
    ? PACKAGE_MANAGER_PNPM4_CONFIG
    : PACKAGE_MANAGER_PNPM3_CONFIG,
  yarn: {
    install: [],
    add: ["add"],
    upgrade: ["upgrade"],
    remove: ["remove"],
  },
};

const runCommand = (bin, type, args, ops) => {
  return spawnCommand(
    [
      bin,
      ...(type !== "default" ? PACKAGE_MANAGER_CONFIG[bin][type] : []),
      ...(args || []),
      ...(ops && ops.register ? ["--registry", ops.register] : []),
    ].join(" "),
    {
      ...(ops && ops.cwd ? { cwd: ops.cwd } : {}),
      shell: true,
    }
  );
};

const packageManager = (cwd) => {
  let bin = "npm";
  cwd = cwd || process.cwd();
  if (hasProjectYarn(cwd)) {
    bin = "yarn";
  } else if (hasProjectPnpm(cwd)) {
    bin = "pnpm";
  } else if (hasProjectNpm(cwd)) {
    bin = "npm";
  }

  return {
    register(source) {
      return source
        ? runCommand(bin, "default", ["config", "set", "registry", source])
        : runCommand(bin, "default", ["config", "get", "registry"]);
    },
    install(args, ops) {
      return runCommand(bin, "install", args, ops);
    },
    add(pkgName, args, ops) {
      return runCommand(bin, "add", [pkgName, ...args], ops);
    },
    remove(pkgName, args, ops) {
      return runCommand(bin, "remove", [pkgName, ...args], ops);
    },
    upgrade(pkgName, args, ops) {
      return runCommand(bin, "upgrade", [pkgName, ...args], ops);
    },
    run(args, ops) {
      return runCommand(bin, "default", [...args], ops);
    },
  };
};

const spawnCommand = (command, ops) => {
  utilError([command].join(" "), "warn");
  return new Promise((resolve, reject) => {
    const result = child_process.spawn(command, ops);
    result.stdout.on("data", (data) => {
      utilError({ message: data }, "tip", true);
    });
    result.stderr.on("data", (data) => {
      utilError({ message: data }, "warn", true);
    });
    result.on("close", (code) => {
      resolve();
    });
  });
};

module.exports = {
  color: chalk,
  packageManager,
  parseCode(code) {
    switch (code) {
      case "zh":
        return "chinese";
      case "en":
        return "english";
      case "ru":
        return "russian";
    }
  },
  isLinux,
  isMacintosh,
  isWindows,
  cpFile(from, to) {
    return new Promise((resolve, reject) => {
      fs.copyFile(from, to, (err) => {
        if (err) {
          reject(err);
          return;
        }
        resolve(from);
      });
    });
  },
  cpDir(from, to) {
    return new Promise((resolve, reject) => {
      fs.cp(from, to, { recursive: true }, (err) => {
        if (err) {
          reject(err);
          return;
        }
        resolve(from);
      });
    });
  },
  loading(callback, ops) {
    return new Promise((resolve, reject) => {
      let spinner = ora(ops.start || "start ...");
      spinner.start();
      try {
        callback()
          .then((res) => {
            spinner.text = ops.succeed;
            spinner.succeed();
            resolve(res);
          })
          .catch((err) => {
            spinner.text = ops.fail;
            spinner.fail();
            reject(err);
          });
      } catch (err) {
        spinner.text = ops.fail;
        spinner.fail();
        reject(err);
      }
    });
  },
  log(type, err) {
    log(type, err);
  },
  unixifyPath,
  recurse(rootdir, callback) {
    recurse(rootdir, callback);
  },
  error(err, type) {
    type = type || "warn";
    if (Array.isArray(err)) {
      err.forEach((e) => utilError(e, type));
      return;
    }
    utilError(err, type);
  },
  stringCase(str) {
    return stringCase(str);
  },
  boolCNorEN,
  isHBuilder(context) {
    return /uniapp\(vue[0-9]+,\s*(vite|webpack),\s*/.test(
      context.metadata.projectType[0].toLowerCase()
    );
  },
  isUniapp(context) {
    return /uniapp/.test(context.metadata.projectType[0].toLowerCase());
  },
  isEmptyObject(obj) {
    for (var name in obj) {
      return false;
    }
    return true;
  },
  checkCustomProfile() {
    let templateFilePath =
      process.env.CUSTOMCONFIG &&
      path.resolve(process.cwd(), process.env.CUSTOMCONFIG);
    if (templateFilePath && fs.existsSync(templateFilePath)) {
      return templateFilePath;
    } else if (templateFilePath && !fs.existsSync(templateFilePath)) {
      log("error", {
        message: [
          "The template configuration file does not exist. Please confirm and try again.",
          templateFilePath,
        ].join(" "),
      });
      process.exit(1);
    }
  },
  checkCustomLangPack() {
    let dictFilePath =
      process.env.LANGPACK && path.resolve(process.cwd(), process.env.LANGPACK);
    if (dictFilePath && fs.existsSync(dictFilePath)) {
      try {
        return require(dictFilePath);
      } catch (err) {
        log("error", {
          message: [
            "Failed to read the language pack file. Please confirm and try again.",
            dictFilePath,
          ].join(" "),
        });
        process.exit(1);
      }
    } else if (dictFilePath && !fs.existsSync(dictFilePath)) {
      log("error", {
        message: [
          "The language pack file does not exist. Please confirm and try again.",
          dictFilePath,
        ].join(" "),
      });
      process.exit(1);
    }
  },
  checkNodeVersion(wanted, id) {
    if (
      !semver.satisfies(process.version, wanted, { includePrerelease: true })
    ) {
      log("error", {
        message:
          "You are using Node " +
          process.version +
          ", but this version of " +
          id +
          " requires Node " +
          wanted +
          ".\nPlease upgrade your Node version.",
      });
      process.exit(1);
    } else {
      log("success", { message: ["Node version:", process.version].join(" ") });
    }
  },
  exeCommand(command) {
    utilError(command, "warn");
    return new Promise((resolve, reject) => {
      child_process.exec(command, (err, stdout, stderr) => {
        if (err) {
          reject(err);
          return;
        }
        stdout && utilError(stdout, "tip", true);
        stderr && utilError(stderr, "warn", true);
        resolve();
      });
    });
  },
  exeCommandSpawn(command, ops = {}) {
    return spawnCommand(command, ops);
  },
  exeCommandSynce(command, ops = {}) {
    utilError([command].join(" "), "warn");
    return new Promise((resolve, reject) => {
      const result = child_process.spawnSync(command, ops);
      //console.log(result);
      utilError({ message: result.output.toString() }, "tip", true);
      resolve();
      /*  result.stdout.on("data", (data) => {
        utilError({ message: data }, "tip", true);
      });
      result.stderr.on("data", (data) => {
        utilError({ message: data }, "warn", true);
      });
      result.on("close", (code) => {
        resolve();
      }); */
    });
  },
  parseCommand(command) {},
  fetchRemote(url, dir, target, tip) {
    return new Promise((resolve, reject) => {
      const spinner = ora(tip.join(" ") + " ...");
      spinner.start();
      download(
        url,
        dir,
        {
          clone: true,
        },
        (err) => {
          if (err) {
            spinner.fail(); // wrong :(
            utilError(err, "warn");
            resolve({
              downloadTemp: target.name,
              ...target,
            });
            return;
          } else {
            spinner.succeed(); // ok :)
            resolve({
              downloadTemp: target.name,
              ...target,
            });
          }
        }
      );
    });
  },
  throttle(fn, threshhold = 250) {
    let last, timer;
    return {
      done(...args) {
        let context = this,
          now = +new Date();
        // 如果距离上次执行 fn 函数的时间小于 threshhold，那么就放弃
        // 执行 fn，并重新计时
        if (last && now < last + threshhold) {
          // 保证在当前时间区间结束后，再执行一次 fn
          timer && clearTimeout(timer);
          timer = setTimeout(function () {
            last = now;
            fn(args);
          }, threshhold);
          // 在时间区间的最开始和到达指定间隔的时候执行一次 fn
        } else {
          last = now;
          fn(args);
        }
      },
    };
  },
  getInstalledBrowsers() {
    if (isLinux) {
      browsers.chrome = getLinuxAppVersion("google-chrome");
      browsers.firefox = getLinuxAppVersion("firefox");
    } else if (isMacintosh) {
      browsers.chrome = getMacAppVersion("com.google.Chrome");
      browsers.firefox = getMacAppVersion("org.mozilla.firefox");
    } else if (isWindows) {
      // get chrome stable version
      // https://stackoverflow.com/a/51773107/2302258
      const chromeQueryResult =
        tryRun(
          'reg query "HKLM\\Software\\Google\\Update\\Clients\\{8A69D345-D564-463c-AFF1-A69D9E530F96}" /v pv /reg:32'
        ) ||
        tryRun(
          'reg query "HKCU\\Software\\Google\\Update\\Clients\\{8A69D345-D564-463c-AFF1-A69D9E530F96}" /v pv /reg:32'
        );
      if (chromeQueryResult) {
        const matched = chromeQueryResult.match(/REG_SZ\s+(\S*)$/);
        browsers.chrome = matched && matched[1];
      }

      // get firefox version
      // https://community.spiceworks.com/topic/111518-how-to-determine-version-of-installed-firefox-in-windows-batchscript
      const ffQueryResult = tryRun(
        'reg query "HKLM\\Software\\Mozilla\\Mozilla Firefox" /v CurrentVersion'
      );
      if (ffQueryResult) {
        const matched = ffQueryResult.match(/REG_SZ\s+(\S*)$/);
        browsers.firefox = matched && matched[1];
      }
    }

    return browsers;
  },
};
